Liquidity Profile Analysis¶
Import Libraries¶
In [1]:
import requests
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import json
import math
import datetime
from tqdm import tqdm
Configuration¶
In [2]:
# # Load configuration
# with open("WETH_USDC_arbitrum_3000_config.json", 'r') as f:
# config = json.load(f)
config = {
"base_symbol": "0",
"base_token": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
"quote_token": "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8",
"decimal_0": "18",
"decimal_1": "6"
}
# Determine token order
if config["base_symbol"] == "0":
token_0, token_1 = config['base_token'], config['quote_token']
else:
token_0, token_1 = config['quote_token'], config['base_token']
# Example Pool Addresses
NETWORK_MAINNET = 'eth_mainnet'
MAINNET_USDC_30BP = '0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8'
MAINNET_USDC_5BP = '0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640'
NETWORK_ARBITRUM = 'arbitrum'
ARB_USDC_5BP = '0xc6962004f452be9203591991d15f6b388e09e8d0'
# Time range
INI_BLOCK = 155687991
FIN_BLOCK = 176212562
Helper Functions¶
In [3]:
def transform_amount_0(amount0):
return int(amount0) * 10 ** -int(config["decimal_0"])
def transform_amount_1(amount1):
return int(amount1) * 10 ** -int(config["decimal_1"])
def tick_2_price(tick):
decimal_0 = int(config["decimal_0"])
decimal_1 = int(config["decimal_1"])
base_symbol = config["base_symbol"]
token_0_price = 1.0001 ** tick * 10 ** (decimal_0 - decimal_1)
return token_0_price if base_symbol == "0" else 1 / token_0_price
def price_2_tick(price):
decimal_0 = int(config["decimal_0"])
decimal_1 = int(config["decimal_1"])
base_symbol = config["base_symbol"]
token_0_price = price if base_symbol == "0" else 1 / price
if token_0_price == 0:
return -math.inf
tick = math.log(token_0_price, 1.0001) + (decimal_1 - decimal_0) * math.log(10, 1.0001)
return round(tick)
def get_datetime_from_blocknumber(block_number):
arbitrum_node_url = "https://arb-mainnet.g.alchemy.com/v2/nHzD3Ofjd2yRam6T9HYBjgtIqp2l8i2K"
payload = {
"jsonrpc": "2.0",
"method": "eth_getBlockByNumber",
"params": [hex(block_number), False],
"id": 1
}
try:
response = requests.post(arbitrum_node_url, json=payload)
response.raise_for_status()
timestamp = int(response.json()["result"]["timestamp"], 16)
return datetime.datetime.utcfromtimestamp(timestamp)
except requests.exceptions.RequestException as e:
print(f"An error occurred: {e}")
return None
API Parameters¶
The API endpoint for fetching liquidity data is:
- URL: http://office-ml.teahouse.finance:8181/pools/liquidity
- Method: GET
- Parameters:
pool: Smart contract address of the liquidity poolblock(optional): Specific block number or "latest" (default: "latest")network(optional): Blockchain network (default: "eth_mainnet")
Time and Price Range¶
In [4]:
# Time range
print(f"Start date: {get_datetime_from_blocknumber(INI_BLOCK)}")
print(f"End date: {get_datetime_from_blocknumber(FIN_BLOCK)}")
# Price range calculation
price = 200 # Example price
tick = price_2_tick(price)
tick_lower = price_2_tick(0.8 * price)
tick_upper = price_2_tick(1.25 * price)
print(f"Tick range: {tick - tick_lower} (lower), {tick_upper - tick} (upper)")
Start date: 2023-12-01 00:00:00 End date: 2024-02-01 00:00:00 Tick range: 2232 (lower), 2231 (upper)
Main Functions¶
In [5]:
def get_LP_df(block, network=NETWORK_ARBITRUM, pool=ARB_USDC_5BP, range_lower=2230, range_upper=2230):
path = f'http://office-ml.teahouse.finance:8181/pools/liquidity?network={network}&block={block}&pool={pool}'
try:
raw = requests.get(path).json()
if raw is None:
raise TypeError(f'API error. No data received. Current block: {block}')
while not raw['success']:
raw = requests.get(path).json()
except requests.exceptions.RequestException as e:
print(f'API connection error: {e}. Current block: {block}')
return None
except TypeError as e:
print(e)
return None
data = raw['data']
current_tick = np.floor(data['tick'] / 10) * 10
tick_lower = current_tick - range_lower
tick_upper = current_tick + range_upper
tick_resample = pd.DataFrame({'ticks': np.arange(np.min(data['ticks']), np.max(data['ticks']) + 10, 10)})
df = pd.DataFrame(data).merge(tick_resample, how='right').ffill()
df = df[df['ticks'].between(tick_lower, tick_upper, 'both')].copy()
df_rotate = df[['ticks', 'liquidity']].T
df_rotate.columns = np.arange(-range_lower, range_upper + 10, 10)
df_rotate = df_rotate.drop('ticks').reset_index().drop(columns='index')
df_rotate.insert(0, 'price', df['price0'].values[0])
df_rotate.insert(1, 'tick', df['tick'].values[0])
df_rotate.index = [block]
return df_rotate
def LP_profile(ini_block=INI_BLOCK, fin_block=FIN_BLOCK, h = 1, network=NETWORK_ARBITRUM, pool=ARB_USDC_5BP):
block_diff = int(h*14400)
df_list = []
#for block in np.arange(ini_block, fin_block + 1, 14400):
for block in tqdm(np.arange(ini_block, fin_block + 1, block_diff)):
df = get_LP_df(block, network, pool)
if df is not None:
df_list.append(df)
df_LP = pd.concat(df_list)
df_LP.index.name = 'blockNumber'
return df_LP
Usage Examples¶
In [6]:
# Example 1: Get liquidity profile for a specific block
lp_data = get_LP_df(250216394, network=NETWORK_ARBITRUM, pool=ARB_USDC_5BP)
print(lp_data.head())
price tick -2230 -2220 \
250216394 2407.857488 -198456.0 65319128404743974 65317708380180194
-2210 -2200 -2190 \
250216394 65317707420994702 65317706479255557 65922475316990807
-2180 -2170 -2160 ... \
250216394 65918421117372642 65918420210575981 65476739682030715 ...
2140 2150 2160 \
250216394 2298778648364058955 2181321853172914261 2181565779301741685
2170 2180 2190 \
250216394 2172484536418228451 2234380006049459731 2166225040768634533
2200 2210 2220 \
250216394 2105930947724359136 2585744329154382117 2678250644703325821
2230
250216394 2576314153690487211
[1 rows x 449 columns]
In [7]:
# Time range
INI_BLOCK = 155687991
FIN_BLOCK = 176212562
# h=5 means use every 5th block. ideally should be binned instead
h = 5
# Example 2: Get liquidity profile over a range of blocks
lp_profile = LP_profile(INI_BLOCK, FIN_BLOCK, h = h)
print(lp_profile.head())
100%|████████████████████████████████████████████████████████████████████████████████| 286/286 [22:36<00:00, 4.74s/it]
price tick -2230 \
blockNumber
155687991 2051.612845 -200057.0 230197839079941088.0
155759991 2094.303730 -199851.0 252267184765966432.0
155831991 2092.044645 -199862.0 252193044622096576.0
155903991 2084.387919 -199898.0 254062828852266784.0
155975991 2092.133036 -199861.0 251100708057670400.0
-2220 -2210 -2200 \
blockNumber
155687991 230197839079941088.0 230197839079941088.0 230210442796650048.0
155759991 252191400256621312.0 253467730047109824.0 253467730047109824.0
155831991 252267184765966432.0 252191400256621312.0 253467730047109824.0
155903991 254020379421091008.0 251452975186504192.0 252090772447810784.0
155975991 251174848201540256.0 251099063692195136.0 252375393482683616.0
-2190 -2180 -2170 \
blockNumber
155687991 230418736520182624.0 253238506699890048.0 254247675409071712.0
155759991 253467730047109824.0 252880119352108384.0 254549883221558080.0
155831991 253467730047109824.0 253467730047109824.0 252880119352108384.0
155903991 252164912591680672.0 252089128082335520.0 253365457872824032.0
155975991 252375393482683616.0 252375393482683616.0 251787782787682208.0
-2160 ... 2140 \
blockNumber ...
155687991 256374839332466688.0 ... 256705466883063392.0
155759991 302316489695047296.0 ... 244586593717825472.0
155831991 254549883221558080.0 ... 242465426647041216.0
155903991 253365457872824032.0 ... 242465426647041216.0
155975991 253457546657131904.0 ... 242330657779851712.0
2150 2160 2170 \
blockNumber
155687991 256705466883063392.0 254784560262612448.0 254784560262612448.0
155759991 244586593717825472.0 195433979485914944.0 194484135775501920.0
155831991 242465426647041216.0 242465426647041216.0 193312812415130688.0
155903991 242465426647041216.0 242465426647041216.0 242465426647041216.0
155975991 242330657779851712.0 242330657779851712.0 193178043547941184.0
2180 2190 2200 \
blockNumber
155687991 244586593717825472.0 244586593717825472.0 244586593717825472.0
155759991 194484135775501920.0 194326396850945344.0 194326396850945344.0
155831991 192362968704717696.0 192362968704717696.0 192205229780161120.0
155903991 242465426647041216.0 242465426647041216.0 193312812415130688.0
155975991 192228199837528192.0 192228199837528192.0 192205229780161120.0
2210 2220 2230
blockNumber
155687991 244586593717825472.0 244586593717825472.0 244586593717825472.0
155759991 194326396850945344.0 194326396850945344.0 194326396850945344.0
155831991 192205229780161120.0 192205229780161120.0 192205229780161120.0
155903991 192362968704717696.0 192362968704717696.0 192205229780161120.0
155975991 192205229780161120.0 192205229780161120.0 192205229780161120.0
[5 rows x 449 columns]
In [8]:
# Example 3: Visualize liquidity distribution
plt.figure(figsize=(12, 6))
plt.plot(lp_data.columns[2:], lp_data.iloc[0, 2:])
plt.title('Liquidity Distribution')
plt.xlabel('Tick')
plt.ylabel('Liquidity')
plt.show()
(Tick, Time) -> Liquidity Surface¶
Visualization¶
In [9]:
lp_profile
# Constants
SECONDS_IN_YEAR = 365.25 * 24 * 3600 # 31,557,600 seconds
BLOCK_TIME_SECONDS = 1 # Arbitrum block time
# Determine the first block number
first_block = lp_profile.index.min()
# Calculate time in years from the first observation
lp_profile['time'] = (lp_profile.index - first_block) * BLOCK_TIME_SECONDS / SECONDS_IN_YEAR
In [21]:
# Reset index to have 'block' as a column
df_reset = lp_profile.reset_index().rename(columns={'index': 'blockNumber'})
# Melt the DataFrame to long format
df = df_reset.melt(id_vars=['blockNumber', 'price', 'tick', 'time'],
var_name='tick_level',
value_name='liquidity')
df['liquidity'] = pd.to_numeric(df['liquidity'])
In [24]:
# Transform the 'tick_level' from string to integer
df['tick_level'] = df['tick_level'].astype(int)
# Add the 'y' column
df['y'] = np.log(df['liquidity'])
print(df.head())
print(df.tail())
blockNumber price tick time tick_level liquidity \
0 155687991 2051.612845 -200057.0 0.000000 -2230 2.301978e+17
1 155759991 2094.303730 -199851.0 0.002282 -2230 2.522672e+17
2 155831991 2092.044645 -199862.0 0.004563 -2230 2.521930e+17
3 155903991 2084.387919 -199898.0 0.006845 -2230 2.540628e+17
4 155975991 2092.133036 -199861.0 0.009126 -2230 2.511007e+17
y
0 39.977716
1 40.069265
2 40.068971
3 40.076358
4 40.064630
blockNumber price tick time tick_level \
127837 175919991 2337.341007 -198753.0 0.641113 2230
127838 175991991 2335.387048 -198761.0 0.643395 2230
127839 176063991 2300.651759 -198911.0 0.645676 2230
127840 176135991 2342.705172 -198730.0 0.647958 2230
127841 176207991 2285.705318 -198976.0 0.650240 2230
liquidity y
127837 3.928340e+17 40.512164
127838 3.956828e+17 40.519389
127839 4.407689e+17 40.627297
127840 3.919023e+17 40.509789
127841 4.534801e+17 40.655728
3D Surface Plot using Matplotlib¶
In [25]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D # Import the 3D plotting toolkit
# Assuming 'df' is your DataFrame with columns 'tick', 'time', and 'y'
# Create a pivot table to reshape the data for plotting
pivot_df = df.pivot(index='time', columns='tick_level', values='y')
# Generate meshgrid for plotting
X, Y = np.meshgrid(pivot_df.columns.values, pivot_df.index.values)
Z = pivot_df.values
# Create a 3D plot
fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111, projection='3d')
# Plot the surface
surf = ax.plot_surface(X, Y, Z, cmap='viridis', edgecolor='none')
# Add color bar and labels
fig.colorbar(surf, ax=ax, shrink=0.5, aspect=5)
ax.set_xlabel('Tick')
ax.set_ylabel('Time')
ax.set_zlabel('log(Liquidity)')
ax.set_title('3D Surface Plot of log(Liquidity)')
plt.show()
Contour Plot using Matplotlib¶
In [26]:
import matplotlib.pyplot as plt
# Create contour plot
plt.figure(figsize=(12, 8))
cp = plt.contourf(X, Y, Z, levels=50, cmap='viridis')
plt.colorbar(cp, label='log(Liquidity)')
plt.xlabel('Tick')
plt.ylabel('Time')
plt.title('Contour Plot of log(Liquidity)')
plt.show()
Heatmap using Seaborn¶
In [27]:
import seaborn as sns
import matplotlib.pyplot as plt
# Using the pivoted DataFrame from the previous example
plt.figure(figsize=(14, 10))
sns.heatmap(pivot_df, cmap='viridis', cbar_kws={'label': 'log(Liquidity)'})
plt.xlabel('Tick')
plt.ylabel('Time')
plt.title('Heatmap of log(Liquidity)')
plt.show()
Interactive 3D Plot using Plotly¶
In [28]:
import plotly.graph_objs as go
import plotly.offline as pyo
# Prepare data for Plotly
fig = go.Figure(data=[go.Surface(z=Z, x=pivot_df.columns.values, y=pivot_df.index.values)])
# Update layout for better visuals
fig.update_layout(
title='Interactive 3D Surface Plot of log(Liquidity)',
autosize=True,
width=800,
height=800,
scene=dict(
xaxis_title='Tick',
yaxis_title='Time',
zaxis_title='log(Liquidity)'
)
)
# Display the plot in the notebook
pyo.iplot(fig)
In [29]:
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
# Get unique time values
unique_times = sorted(df['time'].unique())
# Set up the figure and axis
fig, ax = plt.subplots(figsize=(10, 6))
# Initialize the line object to be updated
line, = ax.plot([], [], lw=2)
# Define the initialization function
def init():
ax.set_xlim(df['tick_level'].min(), df['tick_level'].max())
ax.set_ylim(df['y'].min(), df['y'].max())
ax.set_xlabel('Tick')
ax.set_ylabel('log(Liquidity)')
ax.set_title('Liquidity over Ticks')
return line,
# Define the animation function
def animate(i):
data = df[df['time'] == unique_times[i]]
line.set_data(data['tick_level'], data['y'])
ax.set_title(f'Liquidity over Ticks at Time {unique_times[i]}')
return line,
# Create the animation
ani = FuncAnimation(fig, animate, frames=len(unique_times), init_func=init, blit=True)
# Display the animation in the notebook
HTML(ani.to_jshtml())
Out[29]: